<?php 
/**
 * This file is part of DomAr.
 * 
 * DomAr is free software: you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 * 
 * DomAr is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 * 
 * You should have received a copy of the GNU General Public License
 * along with DomAr.  If not, see <http://www.gnu.org/licenses/>.
 * 
 * @link http://inditel.ee/
 * @copyright 2007-2009 Inditel Meedia OÜ
 * @author Oliver Leisalu <oliver@inditel.ee>
 */


/**
* Plugin that helps to handle one file.
*
* Notice that any changes you make to the files are done immediately. If you don't save you object after changing files then next time you load object file links are broken.
* So you should save your object everytime you make changes to file. (Be sure that your object validates if calling save())
* 
* Its also useful when dealing with uploaded files check that everything is ok.
* <code>
* if( $ob->)
* </code>
*
* Using annotation
* <code>
* @orm plugin DomArFile({
* 	allowedExtensions={gif,jpg,jpeg,png},
* 	root=someroot,
* 	rootUrl=someRootUrl,
*   relative=someRelativePath,
* 	beforeadd={methodName[,{arg1,arg2,...}]},
* 	afteradd={methodName[,{arg1,arg2,...}]}),
* 	autoRename=String $mask,
* </code>
* All arguments are optional.
* <ul>
* <li> beforeadd,afteradd listeners allow you to create custom method for listening when new file is added/uploaded.</li>
* <li> autoRename - Autorenames file using custom mask after object is saved. See method autoRename for details how to write mask. <br>
* 		NB! When using autoRename notice that file will be renamed during save process. Before that custom name is used.
* </li>
* </ul>
*
* Validation errors and codes: (1-50 are reserved for DomArFile, 51-99 are free for extends.)
* <ul>
* <li>12 - added file extension is not allowed</li>
* </ul>
* 
* @package DomAr
* @subpackage Plugin
*/
class DomArFile extends DomArPlugin {

	/**
	* @var String
	*/
	private $path;
	
	/**
	* @var String root path of file that always remains same.
	*/
	private $root;
	
	/**
	* @var String url path relative to $path. If not possible leave empty.
	*/
	private $rootUrl;
	
	/**
	* @var Array allowed file extensions
	*/
	protected $allowedExtensions;

	/**
	* @param String $path file path.
	*/
	public function __construct( $path = NULL, array $allowedExtensions = array(), $root = DOMAR_FILE_DEFAULT_PATH, $rootUrl = DOMAR_FILE_DEFAULT_URL ) {
		$this->setAllowedExtensions( $allowedExtensions );
		$this->setRoot($root);
		$this->setRootUrl($rootUrl);
		$this->path = $path;
	}
	/**
	* Add uploaded file
	* <code>
	* $f->addUploaded( $_FILES['tmp_name'], $_FILES['name'], 'myDir/myFile');
	* </code>
	* @param String $tmpPath temporary path where file is located.
	* @param String $name uploaded file name
	* @param String $newName File new name that was uploaded. (NB! without extension). If NULL then $name is used.
	* @param Bool $unlinkIfExists Unlink target file (before adding new file) if it exists.
	* @throws InvalidArgumentException if file upload is not allowed and has no parent. If has parent validation error with code 10 is raised!
	*/
	public function addUploaded( $tmpPath, $name, $newRelativePath = NULL, $unlinkIfExists = false ) {
		
		if( !is_uploaded_file($tmpPath) ) return false;

		// preparing new name.
		if( $newRelativePath !== NULL )
			$newRelativePath .= '.'.array_pop( explode('.',$name) );
		else
			$newRelativePath = $name;
			
		return $this->_addFile( $tmpPath, $newRelativePath, $unlinkIfExists );	
	}
	
	/**
	* File is moved into $root path.
	* Be aware that if you use dot in your file name then you must add file extension as part after last dot is thought to be extension.
	* <code>
	* $f->addExisting( '/root/sourceFile.txt', 'myDir/myNewFile');
	* // File is copied from '/root/sourceFile.txt' to $f->root.'myDir/myNewFile.txt'
	* </code>
	* @param String $path file full path
	* @param String $name file new path without extension. If not added file name is taken from original file. 
	* @param Bool $unlinkIfExists Unlink target file (before adding new file) if it exists.
	* @throws InvalidArgumentException if file upload is not allowed and has no parent. If has parent validation error with code 10 is raised!
	*/
	public function addExisting( $path, $newRelativePath = NULL, $unlinkIfExists = false, $copy = true ) {	
		
		if( !is_file($path) ) return false;
		
		$path = str_replace('\\','/',$path);
		
		if( $newRelativePath === NULL ) {
			// set previous file name.
			$newRelativePath = array_pop(explode('/', $path));
		} else {
			// adding file extension to name
			$newRelativePath .= '.'.array_pop(explode( '.', $path));
		}
		
		return $this->_addFile( $path, $newRelativePath, $unlinkIfExists, $copy );
	}
	/**
	 * Adds file
	 *
	 * @param String $path
	 * @param String $newRelativePath name with extension
	 */
	private function _addFile( $path, $newRelativePath, $unlinkIfExists = false, $copy = true ) {
		
		$info = self::pathInfo( $newRelativePath );
		$info['basename'] = self::repairName( $info['basename'] );

		// check that file is safe.
		if( $this->isExtensionSafe($info['extension']) === false ) {
			if( $this->getParent() )
				$this->getParent()->registerFailedValidation(12, $this->getProperty()->name, '"Added uploaded file type ('.$info['extension'].') is not allowed"');
			return false;
		}
		
		// init before add listener
		$property = $this->getProperty();
		if( $property && $property->listeners['beforeadd'] )
			call_user_func_array( array( $this->getParent(), $property->listeners['beforeadd'][0] ), array_merge(array($path), $property->listeners['beforeadd'][1]) );

			
		
		$newPath = $info['dirname'] . $info['basename'];
		
		$this->unlink(); // delete old file.
		
		// unlink if target file exists and allowed or create new tmp name.
		if( file_exists($this->root . $newPath) ) {
			if( $unlinkIfExists === true )
				unlink( $this->root . $newPath );
			else
				$newPath = $this->makeTmpFileName($newPath);
		}
		$this->path = $newPath;

		$this->createDirectoryTree( $info['dirname'] );
		
		if( $copy === false ) {
			rename( $path, $this->getPath() );
		} else {
			copy( $path, $this->getPath() );
		}
		chmod( $this->getPath(), 0777);

		$this->changed();
		
		
		
		// init after add listener
		$property = $this->getProperty();
		if( $property && $property->listeners['afteradd'] )
			call_user_func_array( array( $this->getParent(), $property->listeners['afteradd'][0] ), $property->listeners['afteradd'][1] );
			
		
		return true;	
	}
	/**
	* Renames file relative dir. File location must be relative to root path. If you add filename that will also be changed.
	* $file->rename('newFileName');
	* $file->rename('newFileName.newExtension');
	* $file->rename('/newFileName.newExtension'); move file to rootPath with newName.
	* $file->rename('newPath/to/file/');
	* $file->rename('newPath/to/file/newName');
	* $file->rename('newPath/to/file/newName.asdf'); //everything after one dot is considered extension. So this will rename file to newName.asdf NOT to newName.asdf.txt
	* $file->rename('newPath/to/file/newName.txt');
	* @param String $newPath
	* @return Boolean
	*/
	public function rename( $newRelativePath, $unlinkIfExists = false ) {
	
		$info = self::pathInfo( $newRelativePath );
		
		$fl = substr($newRelativePath,0,1);
		if( !$info['dirname'] && $fl != '/' && $fl != '\\')
			$info['dirname'] = $this->getDir();
		
		if( !$info['extension'] ) {
			$info['extension'] = $this->getExt();
			$info['basename'] = $info['filename'].'.'.$info['extension'];
		}
		
		if( !$info['filename'] ) {
			$info['filename'] = $this->getName();
			$info['basename'] = $info['filename'].'.'.$info['extension'];
		}
		
		if( $this->isExtensionSafe($info['extension']) === false )
			throw new RuntimeException('Extension "'.$info['extension'].'" is not allowed!');
		
		$newPath = $info['dirname'] . $info['basename'];
		
		if( $this->path != $newPath ) {
			if( file_exists( $this->root . $newPath ) === true ) {
				if( $unlinkIfExists === true ) {
					trace("UNLINK: ".$this->root . $newPath);
					unlink( $this->root . $newPath );
				} else {
					$newPath = $this->makeTmpFileName( $newPath );
				}
			}
			
			
			$this->createDirectoryTree( $info['dirname'] );
			
			$oldPath = $this->path;
			$this->setPath( $newPath );
			
			trace('RENAME: '.$this->root . $oldPath.', '.$this->getPath());
			rename( $this->root . $oldPath, $this->getPath() );
			try {chmod( $this->getPath(), 0777);} catch( Exception $e ) {}
			$this->cleanDirectoryTree( $oldPath );
		}
	}
	
	/**
	 * Auto renames file name using parent.
	 * Use mask to set rename format. You can use:
	 * <ul>
	 * <li>$className - will be replaced by className</li>
	 * <li>$id - will be replaced by parents id</li>
	 * <li>$idTree - will construct a catalogue tree using $id. Tree will be something like millions/thousands/id. For example item with id 1234567 would be 1/234/1234567. Use this if you expect lots of files.</li>
	 * <li>$propertyName - will be replaced by property name</li>
	 * <li>$rand - will be replaced by random number from 100 to 999</li>
	 * <li>$method(methodName) - will call object method named $method and expects string to be returned.
	 * </ul>
	 * <code>
	 * $file->autoRename( 1 ); // mask $className/$id-$propertyName will be used
	 * $file->autoRename( 2 ); // mask $className/$id/$propertyName will be used
	 * $file->autoRename( '1r' ); // mask $className/$id/$propertyName-$rand will be used
	 * $file->autoRename( '2r' ); // mask $className/$id/$propertyName-$rand will be used
	 * $file->autoRename( 'idTree' ); // mask $className/$idTree/$id/$propertyName will be used
	 * $file->autoRename( 'idTreeR' ); // mask $className/$idTree/$id/$propertyName-$rand will be used
	 * $file->autoRename( 'myDir/$className/s_$id-$propertyName' );
	 * $file->autoRename( 'myDir/$className/$method(methodName)' );
	 * </code>
	 * NB! This method unlinks target file if it exists.
	 * @param String $mask
	 * @return Bool
	 */
	public function autoRename( $mask ) {
		
		if( !$this->exists() )
			return false;
		
		$parent = $this->getParent();
		if( !$parent )
			return false;
		

		if( $mask == '' or $mask === '1' )
			$mask = '$className/$id-$propertyName';
		elseif( $mask == 2 )
			$mask = '$className/$id/$propertyName';
		elseif( $mask == '1r' )
			$mask = '$className/$id-$propertyName-$rand';
		elseif( $mask == '2r' )
			$mask = '$className/$id/$propertyName-$rand';
		elseif( $mask == 'idTree' )
			$mask = '$className/$idTree/$id/$propertyName';
		elseif( $mask == 'idTreeR' )
			$mask = '$className/$idTree/$id/$propertyName-$rand';
		
		$autoName = $mask;

		if( $autoName != null && strpos($autoName, '$method(') !== false ) {
			$methodName = explode('$method(', $autoName);
			$methodName = array_shift(explode(')', $methodName[1]));
			$autoName = str_replace('$method('.$methodName.')',$parent->$methodName(), $autoName );
		}
		
		$autoName = str_replace('$idTree',self::makeIdTree($parent->id), $autoName );
		$autoName = str_replace('$className',get_class($parent), $autoName );
		$autoName = str_replace('$id',$parent->id, $autoName );
		$autoName = str_replace('$propertyName',$this->getProperty()->name, $autoName );
		$autoName = str_replace('$rand',mt_rand(100,999), $autoName );
		
		if( $autoName != $this->path )
			$this->rename( $autoName, true);

		return true;
	}
	
	/**
	 * Makes id tree for autorename IdTree parameter.
	 * @param Int $number
	 * @return string
	 */
	public static function makeIdTree( $number ) {
		
		$millions = (int)($number/1000000);
		$number -= $millions*1000000;
		$thousands = (int)($number/1000);
		
		$millions = str_pad($millions, 7, "0", STR_PAD_LEFT);
		$thousands = str_pad($thousands, 4, "0", STR_PAD_LEFT);
		
		return $millions.'/'.$thousands;
	} 
	
	/**
	* Makes copy of file. Internally makes clone of caller DomFile and copies file to new location.
	* Same as $a = clone $fileObject
	* @param String $newRelativePath
	* @return DomFile 
	*/
	public function copy( $newRelativePath = NULL ) {
		$copy = clone $this;
		if( $newRelativePath !== NULL ) {
			try {
				$copy->rename( $newRelativePath );
			} catch( Exception $e ) {
				$copy->unlink();
				throw $e;
			}
		}
		return $copy;
	}

	/**
	* Deletes file.
	* @return Boolean
	*/
	public function delete() {
		if( $this->path == NULL)
			return true;

		if( $this->exists() ) {
			unlink( $this->getPath() );
			$this->cleanDirectoryTree( $this->getRelativePath() );
		}
		$this->path = NULL;
		$this->changed();
		
		return true;
	}
	
	/**
	* Synonym for delete.
	*/
	public function unlink() {
		$this->delete();
	}
	
	/**
	* if file exists
	* @return Boolean
	*/
	public function exists() {
		return is_file($this->getPath());
	}

	/**
	* Makes clone of DoMArFile object and copies file. 
	*/
	public function __clone() {
		$this->changeParent( NULL, NULL );
		if( $this->exists() ) {
			$newPath = self::makeTmpFileName( $this->getRelativePath() );
			copy( $this->getPath(), $this->root . $newPath );
			$this->path = $newPath;
		} else {
			$this->path = '';
		}
	}
	
	
	 
	/**
	* Creates directory tree relative to root path.
	* @param String $relativeDir
	* @return Bool
	*/
	public function createDirectoryTree( $relativeDir ) {
		if( is_dir($this->root. $relativeDir) === false ) {
			$relativeDir = str_replace('\\','/', $relativeDir);
			$folders = explode( '/', $relativeDir );
			$dir = $this->root;
			while( count($folders) > 0 ) {
				if( is_dir($dir) === false ){
					mkdir( $dir );
					chmod( $dir, 0777 );
				}
				$dir .= ($dir != '' ? DIRECTORY_SEPARATOR : '') . array_shift($folders);
			}
		}
		return true;
	}
	
	/**
	 * Removes folders that containt no files or dirs on given path. Used after file is deleted.
	 *
	 * @param String $relativeDir
	 * @return Bool
	 */
	public function cleanDirectoryTree( $relativePath ) {
		$info = self::pathInfo($relativePath);
		$relativeDir = $info['dirname'];
		
		if( $relativeDir == '' ) return;
		
		$relativeDir = str_replace('\\','/', $relativeDir);
		
		$done = false;
		$folders = explode( '/', $relativeDir );
		$dir = $this->root . $relativeDir;
		while( is_dir($dir) === true && count($folders) > 0 && count(glob($dir.'/*')) == 0 ) {
			$dir = $this->root . implode('/',$folders).'/';
			rmdir( $dir );
			array_pop($folders);
		}
		
		return true;
	}
	
	/**
	* Makes new file name. If file exists on path tries to find file name that is unique.
	* @param String $relativePath relative path to file.
	* @return String New path 
	*/
	public function makeTmpFileName( $relativePath ) {
		if( file_exists( $this->root . $relativePath) === true ) {
			$info = self::pathInfo( $relativePath );
			$i = 0;
			while( true ) {
				$name = $info['dirname'] . $info['filename'] . ($i == 0 ? '' : '('.$i.')') . ($info['extension'] ? '.' . $info['extension'] : NULL);
				if( file_exists($this->root . $name) === false ) return $name;
				$i++;
			}
		}
		return $relativePath;
	}
	
	/**
	* If path is safe
	* @param String $path Dir or filename .
	* @return Boolean
	*/
	public function isPathSafe( $path ) {
		$info = self::pathInfo( $path );

		// : is allowed because it might be hard drive address. (C:/)
		if( $info['dirname'] != '.' && (!preg_match("/^[A-Za-z0-9;:_.,'\/\\() -]*$/", $info['dirname']) or strstr( $info['dirname'], '../') or strstr( $info['dirname'], '..\\')) )
			return false;

		if( $info['filename'] != '' && !$this->isFileNameSafe($info['filename']) )
			return false;

		if( $info['extension'] != '' && !$this->isExtensionSafe($info['extension']) )
			return false;

		return true;
	}
	
	/**
	* If filename is safe. Doesn't check if extension is allowed!
	* @param String $path Dir or filename .
	* @return Boolean
	*/
	public function isFileNameSafe( $fileName ) {
		return (boolean)preg_match("/^[A-Za-z0-9;_.,'() -]*$/", $fileName);
	}
	
	/**
	* If file extension is allowed
	* @param String $ext
	* @return Boolean
	*/
	public function isExtensionSafe( $ext ) {
		return in_array(strtolower($ext), $this->allowedExtensions );
	}
	
	/**
	* Almost same as PHP function pathinfo with some differences:
	* 1) dirname is returned with ending slash if dirname exists. (for path aa/bb/cc.txt pathinfo returns dirname aa/bb, but this method returns aa/bb/)
	* 2) if path ends with slash its assumed to be dir, not filepath. (pathinfo for path aa/bb/cc/ returns filename cc, but this method returns dirname aa/bb/cc/ and filename is empty.
	* @param String $path
	* @return Array
	*/
	public static function pathInfo( $path ) {

		$info = pathinfo($path);
		
		$s = substr( $path, -1, 1);
		if( ($s == '/' or $s == '\\') && strlen($path) > 1 ) {
		//$len = strlen($path);
		//if( $len > 1 && ($path[$len] == '/' or $path[$len] == '\\') ) {
			$info['dirname'] .= '/' . $info['basename'] . ($info['extension'] ? '.'.$info['extension'] : '');
			$info['basename'] = $info['filename'] = $info['extension'] = '';
		}
		
		if( $info['dirname'] == '.' or $info['dirname'] == '/' or $info['dirname'] == '\\')
			$info['dirname'] = '';
		else
			$info['dirname'] .= '/';

		return $info;

		/*
		//does same thing as above but is slower.
		$path = str_replace('\\','/', $path );
		$p = explode('/', $path );
		$result = array();
		$result['basename'] = array_pop($p);
		$result['dirname'] = implode('/', $p ) . (count($p) > 0 ? '/' : '');
		$f = explode('.',$result['basename']);
		$result['extension'] = count($f) > 1 ? array_pop($f) : '';
		$result['filename'] = implode('.',(array)$f);
	//	return $result;
		debug($result);
		*/

	}
	
	/**
	 * Checks that filename is ok
	 * 
	 * Basicly checks that all letters in file  name are allowed. If not allowed letter is skipped or replaced.
	 *
	 * @param String $name
	 * @return String Repaired name
	 */
	public static function repairName( $name ) {
		$replLetters = array('ä' => 'a','ö' => 'o','ü' => 'u','õ' => 'o','Ä' => 'A','Ö' => 'O','Ü' => 'U','Õ' => 'O');
		$newName = '';
		for( $i=0; $i<strlen($name); $i++ ) {
			
			if( isset($replLetters[$name[$i]]) === true ) 
				$name[$i] = $replLetters[$name[$i]];
			else if( isset($replLetters[substr($name,$i,2)]) === true )
				$name[++$i] = $replLetters[substr($name,$i-1,2)];
			
			if( self::isFileNameSafe($name[$i]) ) {
				$newName .= $name[$i];
			}
		}
		return $newName;
	}
	
	
	
	
		
	/**
	* @param String $path file path
	*/
	public function setPath( $path ) {
		if( $this->isPathSafe( $path ) === true )
			$this->path = $path;
		else
			throw new InvalidArgumentException('Path <strong>'.$path.'</strong> is not allowed! Contains not allowed symbols or file extension is not allowed!');
	}
	
	/**
	* @param Array $extensions List with allowed file extensions. Like array('doc','txt', ...)
	*/
	public function setAllowedExtensions( array $extensions ) {
		$this->allowedExtensions = $extensions;
	}
	
	/**
	* If root differs from current root then file path will be renamed to new root location. No dirs will be created, file will be moved to root dir.
	* @param String root path.
	* @param String root url If not specified then assumed that files cannot be accessed from url.
	*/
	public function setRoot( $root, $rootUrl = NULL ) {
		if( $this->root != $root ) {
			// copy file, if it exists, to new root location
			if( $this->path != '' ) {
				$pInfo = self::pathInfo( $this->path );
				$newPath = $root . $pInfo['basename'];
				rename( $this->root . $this->path, $newPath );
				$this->path = $pInfo['basename'];
			}
			$this->root = $root;
		}
		$this->setRootUrl( $rootUrl );
	}
	
	/**
	* @param String root url If not specified then assumed that files cannot be accessed from url.
	*/
	protected function setRootUrl( $rootUrl ) {
		if( $this->root != $root ) {
			$this->rootUrl = $rootUrl;
		}
	}
	
	
	
	
	
	
	
	
	/**
	* Returns file size
	* @return Int
	*/
	public function getFileSize() {
		return filesize($this->getPath());
	}
	
	/**
	* @param String $var
	*/
	public function __get( $var ) {
		switch( $var ) {
			case 'path':
				return $this->getPath();
			break;
			case 'root':
				return $this->getRoot();
			break;
			case 'ext':
				return $this->getExt();
			break;
			case 'name':
				return $this->getName();
			break;
			case 'nameExt':
				return $this->getNameExt();
			break;
			case 'url':
				return $this->getUrl();
			break;
			default:
				throw new RuntimeException('Can\'t access '.$var.' ');
			break;
		}
	}
	
	/**
	* @return String
	*/
	public function getRoot() {
		return $this->root;
	}
	
	/**
	* @return String
	*/
	public function getRootUrl() {
		return $this->rootUrl;
	}
	
	/**
	* @return String File full path
	*/
	public function getPath() {
		return $this->root . $this->path;
	}
	
	/**
	* @return String File url if it is possible to find file from url. Returns Null if there is not file related.
	*/
	public function getUrl() {
		if( $this->rootUrl !== NULL ) {
			if( $this->path ) {
				return $this->rootUrl . $this->path;
			} else {
				return NULL;
			}
		} else {
			throw new BadMethodCallException('Can\'t return Url for file - root url path is not set.');
		}
	}
	/**
	 * @see self::getUrl()
	 * @return String Url encoded url
	 */
	public function getEncodedUrl() {
		if( $this->rootUrl !== NULL ) {
			if( $this->path ) {
				$info = self::pathInfo( $this->path );
				$info['filename'] = rawurlencode($info['filename']);
				return $this->rootUrl . $info['dirname'] . $info['filename'] .'.'. $info['extension'];
			} else {
				return NULL;
			}
		} else {
			throw new BadMethodCallException('Can\'t return Url for file - root url path is not set.');
		}
	}
	/**
	* @return String File relative url to root url. (Internally same as getRelativePath)
	*/
	public function getRelativeUrl() {
		return $this->path;
	}
	/**
	* @return String File extension
	*/
	public function getExt() {
		$info = self::pathInfo( $this->path );
		return $info['extension'];
	}
	
	/**
	* @return String File name without extension
	*/
	public function getName() {
		$info = self::pathInfo( $this->path );
		return $info['filename'];
	}
	
	/**
	* @return String File name with extension
	*/
	public function getNameExt() {
		$info = self::pathInfo( $this->path );
		return $info['basename'];
	}
	
	/**
	* @return String File dir relative to root path.
	*/
	public function getDir() {
		$info = self::pathInfo( $this->path );
		if( $info['dirname'] == '.') return '';
		return $info['dirname'];
	}
	
	/**
	* @return String File relative path to root path.
	*/
	public function getRelativePath() {
		return $this->path;
	}
	
	/**
	* @return Array Allowed extensions
	*/
	public function getAllowedExtensions() {
		return $this->allowedExtensions;
	}
	
	
	/**
	* @param Array $args Currenlty no arguments are parsed!
	* @return String
	*/
	public function toHtml( array $args = array() ) {
		return '<a href="'.$this->getUrl().'">'.$this->path.'</a>';
	}
	
}




/**
* @package DomAr
* @subpackage Plugin
*/
class DomArProperty_DomArFile extends DomArProperty_Plugin implements 
																IDomArPropertyAlterTableListener,
																IDomArPropertyBeforeDeleteListener, 
																IDomArPropertyInsertListener,
																IDomArPropertyUpdateListener,
																IDomArPropertyBeforeSetListener {
	
	/**
	* @param String root path of file that always remains same.
	*/
	protected $root = DOMAR_FILE_DEFAULT_PATH;
	
	/**
	* @param String url path relative to $path. If not possible leave empty.
	*/
	protected $url = DOMAR_FILE_DEFAULT_URL;
	
	/**
	* @param Array allowed file extensions
	*/
	protected $allowedExtensions = array('txt', 'doc');
	
	protected $autoRename = false;
	
	protected $listeners = array();
	
	public function __construct( $name, array $configuration = array() ) {
		parent::__construct( $name );
		if( isset($configuration['autoRename']) === true ) $this->autoRename = $configuration['autoRename'];
		if( isset($configuration['root']) === true ) $this->root = $configuration['root'];
		if( isset($configuration['url']) === true ) $this->url = $configuration['url'];
		if( isset($configuration['relative']) === true ) { $this->root .= $configuration['relative']; $this->url .= $configuration['relative'];}
		if( isset($configuration['allowedExtensions']) === true )$this->allowedExtensions = $configuration['allowedExtensions'];
		if( isset($configuration['beforeadd']) === true ) $this->listeners['beforeadd'] = $configuration['beforeadd'];	
		if( isset($configuration['afteradd']) === true ) $this->listeners['afteradd'] = $configuration['afteradd'];	
	}
	
	/**
	* @param String $type [beforeadd,afteradd]
	* @param String $method
	*/
	public function registerListener( $type, $method, array $args ) {
		$this->listeners[$type] = array($method, $args);
	}
	
	public function init( DomArObject $object, array &$values ) {
		return (string)$values[$this->name];
	}
	
	public function load( DomArObject $object, &$propertyValue ) {
		$d = new DomArFile( $propertyValue, $this->allowedExtensions, $this->root, $this->url, array('beforeadd'=>$this->listeners['beforeadd'],'afteradd'=>$this->listeners['afteradd'])  );
		$d->changeParent( $object, $this );
		return $d;
	}
	
	public function beforeSet( DomArObject $object, &$oldPropertyValue, &$newPropertyValue ) {
		
		if( is_object($oldPropertyValue) )
			$oldPropertyValue->unlink();
		
		
		if( is_object($newPropertyValue) ) {
		
			if( $newPropertyValue->getParent() != NULL )
				throw new RuntimeException('Can\'t add already owned DomArFile object to another parent.');
		
			$newPropertyValue->changeParent( $object, $this );
			$newPropertyValue->setAllowedExtensions( $this->allowedExtensions );
			$newPropertyValue->setRoot( $this->root, $this->url );
		
		} elseif(is_string($newPropertyValue) ) {
			$null = NULL;
			$d = $this->load( $object, $null );
			$d->setPath( $newPropertyValue );
			$newPropertyValue = $d;
		
		} elseif( $newPropertyValue !== NULL ) {
			
			throw new InvalidArgumentException('Invalid argument supplied for '.$this->name.'!');
		}
	}
	
	
	public function isLoaded( DomArObject $object, &$propertyValue ) {
		return is_object($propertyValue);
	}
	
	public function validate( DomArObject $object, &$propertyValue, array &$customParams = array() ) {
		return (is_object($propertyValue) && get_class($propertyValue) == 'DomArFile' or is_string($propertyValue) or $propertyValue === NULL) ? 0 : 1;
	}
	
	public function insert( DomArObject $object, &$propertyValue, DomArValueSet $set   ) {
		$value = is_object($propertyValue) ? $propertyValue->getRelativePath() : $propertyValue;
		
		$set->set( $this->name, ($value != '' ? '\''.addslashes($value).'\'' : 'NULL') );
		
		if( $this->autoRename !== false )
			$object->setChanged( $this->name, true ); // if after insert there are changed property values, update will be automatically made.
	}
	
	public function update( DomArObject $object, &$propertyValue, DomArValueSet $set  ) {
		if( is_object($propertyValue) && $this->autoRename !== false ) {
			$propertyValue->autoRename( $this->autoRename );
			$object->setChanged( $this->name, false ); // we can set it back to unchanged because we will save result to database in next steps.
		}
		$value = is_object($propertyValue) ? $propertyValue->getRelativePath() : $propertyValue;
		$set->set( $this->name, ($value != '' ? '\''.addslashes($value).'\'' : 'NULL') );
	}
	
	
	public function alterTable( DomArTable $table ) {
		return $table->changeColumn( $this->name, 'text', true );
	}
	
	public function beforeDelete( DomArObject $object, &$propertyValue ) {
		$propertyValue->unlink();
	}

}
?>